use moss::from_attrs::FromAttrs;
use moss::from_meta::FromMeta;
use proc_macro2::Span;
use proc_macro2::TokenStream;
use quote::ToTokens;
use quote::quote;
use syn::DeriveInput;
use syn::Expr;
use syn::Field;
use syn::Fields;
use syn::GenericParam;
use syn::Generics;
use syn::Ident;
use syn::Lit;
use syn::LitInt;
use syn::PredicateType;
use syn::Result;
use syn::Token;
use syn::Type;
use syn::TypeParamBound;
use syn::WherePredicate;
use syn::punctuated::Punctuated;

use crate::attrs::AttrType;
use crate::attrs::attrs_tokens_string;

#[derive(FromMeta)]
#[name("reflect")]
pub enum ReflectAttr {
    Skip(Expr),
}

#[derive(FromAttrs)]
pub struct ReflectAttrs {
    pub reflect: Option<ReflectAttr>,
}

pub fn handle_generics_bounds(generics: &mut Generics) -> Result<TokenStream> {
    let mut add_predicate: Punctuated<WherePredicate, Token![,]> = Punctuated::new();
    let mut relation_ty_token = quote!();
    for item in &generics.params {
        if let GenericParam::Type(ty) = item {
            let ty_ident = &ty.ident;
            relation_ty_token = quote! {
                #relation_ty_token <#ty_ident as glass::Reflect>::type_info,
            };
            let mut bounds: Punctuated<TypeParamBound, Token![+]> = Punctuated::new();
            bounds.push(TypeParamBound::Verbatim(quote!(glass::Reflect)));
            add_predicate.push(WherePredicate::Type(PredicateType {
                lifetimes: None,
                bounded_ty: Type::Verbatim(ty.ident.to_token_stream()),
                colon_token: <Token![:]>::default(),
                bounds,
            }));
        }
    }
    let where_clause = generics.make_where_clause();
    where_clause.predicates.extend(add_predicate);
    relation_ty_token = quote! {
        vec![#relation_ty_token]
    };
    Ok(relation_ty_token)
}

pub fn handle_field(field: &Field, index: usize) -> Result<TokenStream> {
    let attrs = attrs_tokens_string(&field.attrs, AttrType::Field);
    let serde_token = attrs.serde;
    let constraits_token = attrs.constraits;
    let name_token = attrs.name;
    let docs_token = attrs.docs;
    let attrs_token = attrs.attrs;
    let mut ident_token = quote!(None);
    if let Some(field_ident) = &field.ident {
        let field_name = field_ident.to_string();
        ident_token = quote!(Some(#field_name.into()));
    }
    let field_ty = &field.ty;
    let index = Lit::Int(LitInt::new(index.to_string().as_str(), Span::call_site()));
    let field_token = quote! {
        glass::Field {
            docs: #docs_token,
            index: #index,
            ident: #ident_token,
            type_info_fn: <#field_ty as glass::Reflect>::type_info,
            attrs: #attrs_token,
            serde: #serde_token,
            constraits: #constraits_token,
            name: #name_token,
        }
    };
    Ok(field_token)
}

fn handle_variant(variant: &syn::Variant, index: usize) -> Result<TokenStream> {
    let attrs = attrs_tokens_string(&variant.attrs, AttrType::Variant);
    let serde_token = attrs.serde;
    let docs_token = attrs.docs;
    let attrs_token = attrs.attrs;
    let ident = &variant.ident;
    let ident_name = variant.ident.to_string();
    let index = Lit::Int(LitInt::new(index.to_string().as_str(), Span::call_site()));
    let mut fields_token = quote!();
    let mut named_token = quote!(false);
    for (index, field) in variant.fields.iter().enumerate() {
        if field.ident.is_some() {
            named_token = quote!(true);
        }
        let field_token = handle_field(field, index)?;
        fields_token = quote! {
            #fields_token
            #field_token,
        };
    }
    fields_token = quote! {
        vec![#fields_token]
    };
    let mut value_token = quote!(None);
    if variant.discriminant.is_some() {
        value_token = quote!(Some(Self::#ident as isize));
    }
    let variant_token = quote! {
        glass::Variant{
            docs: #docs_token,
            index: #index,
            ident: #ident_name.into(),
            fields: glass::Fields {fields: #fields_token, named: #named_token},
            attrs: #attrs_token,
            value: #value_token,
            serde: #serde_token,
        }
    };
    Ok(variant_token)
}

pub struct ConvertGenerator {
    pub try_into_token: TokenStream,
    pub try_from_token: TokenStream,
}

impl ConvertGenerator {
    pub fn expand(variant_ident: Option<&Ident>, fields: &Fields) -> Result<ConvertGenerator> {
        let ty_token = match variant_ident {
            Some(ident) => quote!(Self::#ident),
            None => quote!(Self),
        };
        let mut try_into_token = quote!();
        let mut try_from_token = quote!();

        let mut field_members = quote!();
        for (index, field) in fields.iter().enumerate() {
            let mut value: Option<Expr> = None;
            let attrs = ReflectAttrs::try_from(&field.attrs);
            if let Ok(ReflectAttrs {
                reflect: Some(ReflectAttr::Skip(default)),
            }) = attrs
            {
                value = Some(default);
            }
            match &field.ident {
                Some(ident) => {
                    if let Some(value) = value {
                        try_from_token = quote! {
                            #try_from_token
                            #ident: #value,
                        };
                        continue;
                    }

                    field_members = quote! {
                        #field_members
                        #ident,
                    };
                    let field_name = ident.to_string();
                    let ty = &field.ty;
                    let into_ident = match &variant_ident {
                        Some(_) => quote!(#ident),
                        None => quote!(self.#ident),
                    };
                    try_into_token = quote! {
                        #try_into_token
                        values.insert(
                            #field_name.into(),
                            <#ty as TryInto<glass::Object>>::try_into(#into_ident)?.value,
                        );
                    };
                    try_from_token = quote! {
                        #try_from_token
                        #ident: values
                            .remove(#field_name)
                            .ok_or(glass::ConvertError::missing_field(#field_name))?
                            .try_into()?,
                    };
                }
                None => {
                    if let Some(value) = value {
                        try_from_token = quote! {
                            #try_from_token
                            #value,
                        };
                        continue;
                    }

                    let ident = Ident::new(&format!("_{}", index), Span::call_site());
                    field_members = quote! {
                        #field_members
                        #ident,
                    };
                    let field_name = index.to_string();
                    let ty: &Type = &field.ty;
                    let into_ident = if variant_ident.is_some() {
                        quote! (#ident)
                    } else {
                        let ident = Lit::Int(LitInt::new(&format!("{index}"), Span::call_site()));
                        quote! (self.#ident)
                    };
                    try_into_token = quote! {
                        #try_into_token
                        values.push(
                            <#ty as TryInto<glass::Object>>::try_into(#into_ident)?.value
                        );
                    };
                    try_from_token = quote! {
                        #try_from_token
                        <#ty as TryFrom<glass::Value>>::try_from(
                            values.next().ok_or(glass::ConvertError::missing_field(#field_name))?,
                        )?,
                    };
                }
            }
        }

        let try_into_value;
        let try_from_condition;
        let try_from_kind;
        match fields {
            syn::Fields::Named(_) => {
                try_into_value = quote!(glass::Value::NamedStruct(values));
                try_from_condition = quote!(let glass::Value::NamedStruct(mut values) = value);
                try_from_kind = "NamedStruct";
                try_from_token = quote! {
                    return Ok(#ty_token {
                        #try_from_token
                    })
                };
                try_into_token = quote! {
                    let mut values = std::collections::HashMap::new();
                    #try_into_token
                };
                field_members = quote!({#field_members});
            }
            syn::Fields::Unnamed(_) => {
                try_into_value = quote!(glass::Value::TupleStruct(values));
                try_from_condition = quote!(let glass::Value::TupleStruct(values) = value);
                try_from_kind = "TupleStruct";
                try_from_token = quote! {
                    let mut values = values.into_iter();
                    return Ok(#ty_token (#try_from_token))
                };
                try_into_token = quote! {
                    let mut values = vec![];
                    #try_into_token
                };
                field_members = quote!((#field_members));
            }
            syn::Fields::Unit => {
                try_into_value = quote!(glass::Value::UnitStruct);
                try_from_condition = quote!(let glass::Value::UnitStruct = value);
                try_from_kind = "UnitStruct";
                try_from_token = quote! {
                    return Ok(#ty_token)
                };
            }
        }

        try_from_token = quote! {
            if #try_from_condition {
                #try_from_token
            }
            Err(glass::ConvertError::TypeMismatch {
                expected: #try_from_kind.into(),
                actual: value.to_string(),
            })
        };

        match variant_ident {
            Some(ident) => {
                let match_name = ident.to_string();
                try_into_token = quote! {
                    Self::#ident #field_members => {
                        #try_into_token
                        Ok(glass::Object {
                            type_info_fn: <Self as glass::Reflect>::type_info,
                            value: glass::Value::Enum(String::from(#match_name), Box::new(#try_into_value)),
                        })
                    }
                }
            }
            None => {
                try_into_token = quote! {
                    #try_into_token
                    Ok(glass::Object {
                        type_info_fn: <Self as glass::Reflect>::type_info,
                        value: #try_into_value,
                    })
                };
            }
        }

        Ok(Self {
            try_into_token,
            try_from_token,
        })
    }
}

pub struct ImplGenerator {
    pub into_expanded: TokenStream,
    pub from_expanded: TokenStream,
    pub reflect_expanded: TokenStream,
}

impl ImplGenerator {
    pub fn new(input: &mut DeriveInput) -> Result<Self> {
        let for_ident = &input.ident;
        let attrs = &input.attrs;
        let attrs = attrs_tokens_string(attrs, AttrType::Container);
        let serde_token = attrs.serde;
        let docs_token = attrs.docs;
        let attrs_token = attrs.attrs;
        let generics = &mut input.generics;
        let mut kind_token = handle_generics_bounds(generics)?;
        let mut members = quote!();

        let try_into_token;
        let try_from_token;
        match &input.data {
            syn::Data::Struct(data_struct) => {
                let mut named_token = quote!(false);
                match &data_struct.fields {
                    syn::Fields::Named(_) => {
                        named_token = quote!(true);
                        kind_token = quote! (glass::TypeKind::NamedStruct(#kind_token));
                    }
                    syn::Fields::Unnamed(_) => {
                        kind_token = quote! (glass::TypeKind::TupleStruct(#kind_token));
                    }
                    syn::Fields::Unit => {
                        kind_token = quote! (glass::TypeKind::UnitStruct(#kind_token));
                    }
                }
                for (index, field) in data_struct.fields.iter().enumerate() {
                    let attrs = ReflectAttrs::try_from(&field.attrs);
                    if let Ok(ReflectAttrs {
                        reflect: Some(ReflectAttr::Skip(_)),
                    }) = attrs
                    {
                        continue;
                    }
                    let field_token = handle_field(field, index)?;
                    members = quote! {
                        #members
                        #field_token,
                    };
                }
                members = quote! {
                    glass::Members::Fields(glass::Fields {fields: vec![#members], named: #named_token})
                };
                let convert_generator = ConvertGenerator::expand(None, &data_struct.fields)?;
                try_into_token = convert_generator.try_into_token;
                try_from_token = convert_generator.try_from_token;
            }
            syn::Data::Enum(data_enum) => {
                kind_token = quote! (glass::TypeKind::Enum(#kind_token));
                let mut variant_try_from_token = quote!();
                let mut variant_try_into_token = quote!();
                for (index, variant) in data_enum.variants.iter().enumerate() {
                    let variant_token = handle_variant(variant, index)?;
                    members = quote! {
                        #members
                        #variant_token,
                    };
                    let ident = &variant.ident;
                    let convert_generator = ConvertGenerator::expand(Some(ident), &variant.fields)?;
                    let try_into_token = convert_generator.try_into_token;
                    let try_from_token = convert_generator.try_from_token;

                    let match_name = variant.ident.to_string();
                    variant_try_from_token = quote! {
                        #variant_try_from_token
                        #match_name => {
                            #try_from_token
                        }
                    };
                    variant_try_into_token = quote! {
                        #variant_try_into_token
                        #try_into_token
                    };
                }
                members = quote! {
                    glass::Members::Variants(vec![#members])
                };

                try_from_token = quote! {
                    if let glass::Value::Enum(variant, value) = value {
                        let value = *value;
                        match variant.as_str() {
                            #variant_try_from_token
                            _ => Err(glass::ConvertError::NotExistVariant(variant))
                        }
                    } else {
                        Err(glass::ConvertError::TypeMismatch {
                            expected: String::from("Enum"),
                            actual: value.to_string(),
                        })
                    }
                };
                try_into_token = quote! {
                    match self {
                        #variant_try_into_token
                    }
                };
            }
            syn::Data::Union(_) => {
                return Err(moss::error::Error::unexpected_type("union").into());
            }
        }

        let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
        let from_expanded = quote! {
            impl #impl_generics TryFrom<glass::Value> for #for_ident #ty_generics #where_clause {
                type Error = glass::ConvertError;

                fn try_from(value: glass::Value) -> Result<Self, Self::Error> {
                    #try_from_token
                }
            }
        };
        let into_expanded = quote! {
            impl #impl_generics TryInto<glass::Object> for #for_ident #ty_generics #where_clause {
                type Error = glass::ConvertError;

                fn try_into(self) -> Result<glass::Object, Self::Error> {
                    #try_into_token
                }
            }
        };
        let reflect_expanded = quote! {
            impl #impl_generics glass::Reflect for #for_ident #ty_generics #where_clause {
                fn docs() -> Option<String> {
                    #docs_token
                }

                fn attrs() -> Vec<String> {
                    #attrs_token
                }

                fn kind() -> glass::TypeKind {
                    #kind_token
                }

                fn members() -> glass::Members {
                    #members
                }

                fn serde() -> glass::ContainerSerde {
                    #serde_token
                }
            }
        };
        Ok(Self {
            into_expanded,
            from_expanded,
            reflect_expanded,
        })
    }
}

pub fn expand(input: &mut DeriveInput) -> Result<TokenStream> {
    let impl_generator = ImplGenerator::new(input)?;
    let from_expanded = impl_generator.from_expanded;
    let into_expanded = impl_generator.into_expanded;
    let reflect_expanded = impl_generator.reflect_expanded;
    let tokens = quote! {
        #from_expanded
        #into_expanded
        #reflect_expanded
    };
    Ok(tokens)
}
